// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fuchsia/device/llcpp/fidl.h>
#include <fuchsia/io/llcpp/fidl.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/io.h>
#include <lib/fdio/namespace.h>
#include <lib/fdio/unsafe.h>
#include <string.h>
#include <zircon/device/vfs.h>
#include <zircon/syscalls.h>

#include <mutex>

#include "private-socket.h"

namespace fio = ::llcpp::fuchsia::io;
namespace fsocket = ::llcpp::fuchsia::posix::socket;
namespace fdevice = ::llcpp::fuchsia::device;

#define ZXDEBUG 0

// POLL_MASK and POLL_SHIFT intend to convert the lower five POLL events into
// ZX_USER_SIGNALs and vice-versa. Other events need to be manually converted to
// a zx_signals_t, if they are desired.
#define POLL_SHIFT 24
#define POLL_MASK 0x1F

static_assert(FDIO_CHUNK_SIZE >= PATH_MAX, "FDIO_CHUNK_SIZE must be large enough to contain paths");

static_assert(fio::VMO_FLAG_READ == ZX_VM_PERM_READ, "Vmar / Vmo flags should be aligned");
static_assert(fio::VMO_FLAG_WRITE == ZX_VM_PERM_WRITE, "Vmar / Vmo flags should be aligned");
static_assert(fio::VMO_FLAG_EXEC == ZX_VM_PERM_EXECUTE, "Vmar / Vmo flags should be aligned");

static_assert(ZX_USER_SIGNAL_0 == (1 << POLL_SHIFT), "");
static_assert((POLLIN << POLL_SHIFT) == fdevice::DEVICE_SIGNAL_READABLE, "");
static_assert((POLLPRI << POLL_SHIFT) == fdevice::DEVICE_SIGNAL_OOB, "");
static_assert((POLLOUT << POLL_SHIFT) == fdevice::DEVICE_SIGNAL_WRITABLE, "");
static_assert((POLLERR << POLL_SHIFT) == fdevice::DEVICE_SIGNAL_ERROR, "");
static_assert((POLLHUP << POLL_SHIFT) == fdevice::DEVICE_SIGNAL_HANGUP, "");
static_assert(fio::DEVICE_SIGNAL_READABLE == fdevice::DEVICE_SIGNAL_READABLE);
static_assert(fio::DEVICE_SIGNAL_OOB == fdevice::DEVICE_SIGNAL_OOB);
static_assert(fio::DEVICE_SIGNAL_WRITABLE == fdevice::DEVICE_SIGNAL_WRITABLE);
static_assert(fio::DEVICE_SIGNAL_ERROR == fdevice::DEVICE_SIGNAL_ERROR);
static_assert(fio::DEVICE_SIGNAL_HANGUP == fdevice::DEVICE_SIGNAL_HANGUP);

// The |mode| argument used for |fuchsia.io.Directory/Open| calls.
#define FDIO_CONNECT_MODE ((uint32_t)0755)

// Validates a |path| argument.
//
// Returns ZX_OK if |path| is non-null and less than |PATH_MAX| in length
// (excluding the null terminator). Upon success, the length of the path is
// returned via |out_length|.
//
// Otherwise, returns |ZX_ERR_INVALID_ARGS|.
static zx_status_t fdio_validate_path(const char* path, size_t* out_length) {
  if (path == nullptr) {
    return ZX_ERR_INVALID_ARGS;
  }
  size_t length = strnlen(path, PATH_MAX);
  if (length >= PATH_MAX) {
    return ZX_ERR_INVALID_ARGS;
  }
  *out_length = length;
  return ZX_OK;
}

__EXPORT
zx_status_t fdio_service_connect(const char* path, zx_handle_t h) {
  return fdio_open(path, ZX_FS_RIGHT_READABLE | ZX_FS_RIGHT_WRITABLE, h);
}

__EXPORT
zx_status_t fdio_service_connect_at(zx_handle_t dir, const char* path, zx_handle_t request_raw) {
  zx::channel request(request_raw);
  size_t length = 0u;
  zx_status_t status = fdio_validate_path(path, &length);
  if (status != ZX_OK) {
    return status;
  }

  if (dir == ZX_HANDLE_INVALID) {
    return ZX_ERR_UNAVAILABLE;
  }
  uint32_t flags = ZX_FS_RIGHT_READABLE | ZX_FS_RIGHT_WRITABLE;
  return fio::Directory::Call::Open(zx::unowned_channel(dir), flags, FDIO_CONNECT_MODE,
                                    fidl::StringView(path, length), std::move(request))
      .status();
}

zx_status_t fdio_service_connect_by_name(const char name[], zx::channel* out) {
  static zx_handle_t service_root;

  {
    static std::once_flag once;
    static zx_status_t status;
    std::call_once(once, [&]() {
      zx::channel c0, c1;
      status = zx::channel::create(0, &c0, &c1);
      if (status != ZX_OK) {
        return;
      }
      // TODO(abarth): Use "/svc/" once that actually works.
      status = fdio_service_connect("/svc/.", c0.release());
      if (status != ZX_OK) {
        return;
      }
      service_root = c1.release();
    });
    if (status != ZX_OK) {
      return status;
    }
  }

  zx::channel c0, c1;
  zx_status_t status = zx::channel::create(0, &c0, &c1);
  if (status != ZX_OK) {
    return status;
  }

  status = fdio_service_connect_at(service_root, name, c0.release());
  if (status != ZX_OK) {
    return status;
  }
  *out = std::move(c1);
  return ZX_OK;
}

__EXPORT
zx_status_t fdio_open(const char* path, uint32_t flags, zx_handle_t request) {
  // TODO: fdio_validate_path?
  if (path == nullptr) {
    zx_handle_close(request);
    return ZX_ERR_INVALID_ARGS;
  }
  // Otherwise attempt to connect through the root namespace
  return fdio_ns_connect(fdio_root_ns, path, flags, request);
}

__EXPORT
zx_status_t fdio_open_at(zx_handle_t dir, const char* path, uint32_t flags,
                         zx_handle_t raw_request) {
  zx::channel request(raw_request);
  size_t length;
  zx_status_t status = fdio_validate_path(path, &length);
  if (status != ZX_OK) {
    return status;
  }

  if (flags & ZX_FS_FLAG_DESCRIBE) {
    return ZX_ERR_INVALID_ARGS;
  }

  return fio::Directory::Call::Open(zx::unowned_channel(dir), flags, FDIO_CONNECT_MODE,
                                    fidl::StringView(path, length), std::move(request))
      .status();
}

__EXPORT
zx_status_t fdio_open_fd(const char* path, uint32_t flags, int* out_fd) {
  zx::channel client, server;
  zx_status_t status = zx::channel::create(0, &client, &server);
  if (status != ZX_OK) {
    return status;
  }

  status = fdio_open(path, flags, server.release());
  if (status != ZX_OK) {
    return status;
  }

  int fd;
  status = fdio_fd_create(client.release(), &fd);
  if (status != ZX_OK) {
    return status;
  }

  *out_fd = fd;
  return ZX_OK;
}

__EXPORT
zx_status_t fdio_open_fd_at(int dir_fd, const char* path, uint32_t flags, int* out_fd) {
  zx::channel client, server;
  zx_status_t status = zx::channel::create(0, &client, &server);
  if (status != ZX_OK) {
    return status;
  }

  fdio_t* iodir = fdio_unsafe_fd_to_io(dir_fd);
  if (iodir == NULL) {
    return ZX_ERR_INVALID_ARGS;
  }

  zx_handle_t dir_channel = fdio_unsafe_borrow_channel(iodir);
  if (dir_channel == ZX_HANDLE_INVALID) {
    fdio_unsafe_release(iodir);
    return ZX_ERR_NOT_SUPPORTED;
  }

  status = fdio_open_at(dir_channel, path, flags, server.release());
  fdio_unsafe_release(iodir);
  if (status != ZX_OK) {
    return status;
  }

  int fd;
  status = fdio_fd_create(client.release(), &fd);
  if (status != ZX_OK) {
    return status;
  }

  *out_fd = fd;
  return ZX_OK;
}

__EXPORT
zx_handle_t fdio_service_clone(zx_handle_t handle) {
  if (handle == ZX_HANDLE_INVALID) {
    return ZX_HANDLE_INVALID;
  }
  zx::channel clone, request;
  if (zx::channel::create(0, &clone, &request) != ZX_OK) {
    return ZX_HANDLE_INVALID;
  }
  uint32_t flags = ZX_FS_FLAG_CLONE_SAME_RIGHTS;
  auto result = fio::Node::Call::Clone(zx::unowned_channel(handle), flags, std::move(request));
  if (result.status() != ZX_OK) {
    return ZX_HANDLE_INVALID;
  }
  return clone.release();
}

__EXPORT
zx_status_t fdio_service_clone_to(zx_handle_t handle, zx_handle_t request_raw) {
  zx::channel request(request_raw);
  if (!request.is_valid()) {
    return ZX_ERR_INVALID_ARGS;
  }
  uint32_t flags = ZX_FS_FLAG_CLONE_SAME_RIGHTS;
  return fio::Node::Call::Clone(zx::unowned_channel(handle), flags, std::move(request)).status();
}

// Create an |fdio_t| from a |handle| and an |info|.
//
// Uses |info| to determine what kind of |fdio_t| to create.
//
// Upon success, |out_io| receives ownership of all handles.
//
// Upon failure, consumes all handles.
static zx_status_t fdio_from_node_info(zx::channel handle, fio::NodeInfo info, fdio_t** out_io) {
  if (!handle.is_valid()) {
    return ZX_ERR_INVALID_ARGS;
  }

  fdio_t* io = nullptr;
  switch (info.which()) {
    case fio::NodeInfo::Tag::kDirectory:
      io = fdio_dir_create(handle.release());
      break;
    case fio::NodeInfo::Tag::kService:
      io = fdio_remote_create(handle.release(), 0);
      break;
    case fio::NodeInfo::Tag::kFile:
      io = fdio_file_create(handle.release(), info.mutable_file().event.release());
      break;
    case fio::NodeInfo::Tag::kDevice:
      io = fdio_remote_create(handle.release(), info.mutable_device().event.release());
      break;
    case fio::NodeInfo::Tag::kTty:
      io = fdio_remote_create(handle.release(), info.mutable_tty().event.release());
      break;
    case fio::NodeInfo::Tag::kVmofile: {
      fio::File::SyncClient control(std::move(handle));
      auto result = control.Seek(0, fio::SeekOrigin::START);
      zx_status_t status = result.status();
      if (status != ZX_OK) {
        return status;
      }
      status = result->s;
      if (status != ZX_OK) {
        return status;
      }
      io = fdio_vmofile_create(std::move(control), std::move(info.mutable_vmofile().vmo),
                               info.vmofile().offset, info.vmofile().length, result->offset);
      break;
    }
    case fio::NodeInfo::Tag::kPipe: {
      io = fdio_pipe_create(std::move(info.mutable_pipe().socket));
      break;
    }
    case fio::NodeInfo::Tag::kSocket: {
      // check the connection state.
      zx_signals_t observed;

      switch (zx_status_t status = info.socket().socket.wait_one(
                  ZXSIO_SIGNAL_CONNECTED, zx::time::infinite_past(), &observed)) {
        case ZX_OK:
          __FALLTHROUGH;
        case ZX_ERR_TIMED_OUT:
          break;
        default:
          return status;
      }

      zx_status_t status = fdio_socket_create(fsocket::Control::SyncClient(std::move(handle)),
                                              std::move(info.mutable_socket().socket), &io);
      if (status != ZX_OK) {
        return status;
      }
      if (observed & ZXSIO_SIGNAL_CONNECTED) {
        *fdio_get_ioflag(io) |= IOFLAG_SOCKET_CONNECTED;
      }
      break;
    }
    default:
      return ZX_ERR_NOT_SUPPORTED;
  }

  if (io == nullptr) {
    return ZX_ERR_NO_RESOURCES;
  }

  *out_io = io;
  return ZX_OK;
}

// Creates an |fdio_t| from a Zircon channel object.
//
// The |channel| must implement the |fuchsia.io.Node| protocol. Uses the
// |Describe| method from the |fuchsia.io.Node| protocol to determine the type
// of |fdio_t| object to create.
//
// Always consumes |channel|.
static zx_status_t fdio_from_channel(zx::channel channel, fdio_t** out_io) {
  auto response = fio::Node::Call::Describe(zx::unowned_channel(channel));
  zx_status_t status = response.status();
  if (status != ZX_OK) {
    return status;
  }
  return fdio_from_node_info(std::move(channel), std::move(response.Unwrap()->info), out_io);
}

__EXPORT
zx_status_t fdio_create(zx_handle_t handle, fdio_t** out_io) {
  zx_info_handle_basic_t info;
  zx_status_t status =
      zx_object_get_info(handle, ZX_INFO_HANDLE_BASIC, &info, sizeof(info), nullptr, nullptr);
  if (status != ZX_OK) {
    return status;
  }
  fdio_t* io = nullptr;
  switch (info.type) {
    case ZX_OBJ_TYPE_CHANNEL:
      return fdio_from_channel(zx::channel(handle), out_io);
    case ZX_OBJ_TYPE_SOCKET:
      io = fdio_pipe_create(zx::socket(handle));
      break;
    case ZX_OBJ_TYPE_VMO:
      io = fdio_vmo_create(zx::vmo(handle), 0u);
      break;
    case ZX_OBJ_TYPE_LOG:
      io = fdio_logger_create(zx::debuglog(handle));
      break;
    default: {
      zx_handle_close(handle);
      return ZX_ERR_INVALID_ARGS;
    }
  }
  if (io == nullptr) {
    return ZX_ERR_NO_MEMORY;
  }
  *out_io = io;
  return ZX_OK;
}

zx_status_t fdio_remote_open_at(zx_handle_t dir, const char* path, uint32_t flags, uint32_t mode,
                                fdio_t** out_io) {
  size_t length;
  zx_status_t status = fdio_validate_path(path, &length);
  if (status != ZX_OK) {
    return status;
  }

  zx::channel handle, request;
  status = zx::channel::create(0, &handle, &request);
  if (status != ZX_OK) {
    return status;
  }

  status = fio::Directory::Call::Open(zx::unowned_channel(dir), flags, mode,
                                      fidl::StringView(path, length), std::move(request))
               .status();
  if (status != ZX_OK) {
    return status;
  }

  if (flags & ZX_FS_FLAG_DESCRIBE) {
    fio::NodeInfo node_info;
    zx_status_t on_open_status = ZX_OK;
    status = fio::Directory::Call::HandleEvents(
        zx::unowned_channel(handle),
        fio::Directory::EventHandlers{
            .on_open =
                [&node_info, &on_open_status](zx_status_t status, fio::NodeInfo* info) {
                  on_open_status = status;
                  if (info) {
                    node_info = std::move(*info);
                  }
                  return ZX_OK;
                },
            .unknown = [] { return ZX_ERR_IO; }});
    if (status != ZX_OK) {
      return status;
    }
    if (on_open_status != ZX_OK) {
      return on_open_status;
    }
    return fdio_from_node_info(std::move(handle), std::move(node_info), out_io);
  }

  fdio_t* io = fdio_remote_create(handle.release(), 0);
  if (io == nullptr) {
    return ZX_ERR_NO_RESOURCES;
  }
  *out_io = io;
  return ZX_OK;
}
